package com.rengwuxian.wecompose.ui

import androidx.compose.animation.AnimatedFloatModel
import androidx.compose.animation.core.AnimationClockObservable
import androidx.compose.animation.core.AnimationEndReason
import androidx.compose.animation.core.fling
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.structuralEqualityPolicy
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.ParentDataModifier
import androidx.compose.ui.unit.Density
import kotlin.math.roundToInt

/**
 * This is a modified version of:
 * https://gist.github.com/adamp/07d468f4bcfe632670f305ce3734f511
 */

class PagerState(
  clock: AnimationClockObservable,
  currentPage: Int = 0,
  minPage: Int = 0,
  maxPage: Int = 0
) {
  private var _minPage by mutableStateOf(minPage)
  var minPage: Int
    get() = _minPage
    set(value) {
      _minPage = value.coerceAtMost(_maxPage)
      _currentPage = _currentPage.coerceIn(_minPage, _maxPage)
    }

  private var _maxPage by mutableStateOf(maxPage, structuralEqualityPolicy())
  var maxPage: Int
    get() = _maxPage
    set(value) {
      _maxPage = value.coerceAtLeast(_minPage)
      _currentPage = _currentPage.coerceIn(_minPage, maxPage)
    }

  private var _currentPage by mutableStateOf(currentPage.coerceIn(minPage, maxPage))
  var currentPage: Int
    get() = _currentPage
    set(value) {
      _currentPage = value.coerceIn(minPage, maxPage)
    }

  enum class SelectionState { Selected, Undecided }

  var selectionState by mutableStateOf(SelectionState.Selected)

  inline fun <R> selectPage(block: PagerState.() -> R): R = try {
    selectionState = SelectionState.Undecided
    block()
  } finally {
    selectPage()
  }

  fun selectPage() {
    currentPage -= currentPageOffset.roundToInt()
    currentPageOffset = 0f
    selectionState = SelectionState.Selected
  }

  private var _currentPageOffset = AnimatedFloatModel(0f, clock = clock).apply {
    setBounds(-1f, 1f)
  }
  var currentPageOffset: Float
    get() = _currentPageOffset.value
    set(value) {
      val max = if (currentPage == minPage) 0f else 1f
      val min = if (currentPage == maxPage) 0f else -1f
      _currentPageOffset.snapTo(value.coerceIn(min, max))
    }

  fun fling(velocity: Float) {
    if (velocity < 0 && currentPage == maxPage) return
    if (velocity > 0 && currentPage == minPage) return

    _currentPageOffset.fling(velocity) { reason, _, _ ->
      if (reason != AnimationEndReason.Interrupted) {
        _currentPageOffset.animateTo(currentPageOffset.roundToInt().toFloat()) { _, _ ->
          selectPage()
        }
      }
    }
  }

  override fun toString(): String = "PagerState{minPage=$minPage, maxPage=$maxPage, " +
      "currentPage=$currentPage, currentPageOffset=$currentPageOffset}"
}

@Immutable
private data class PageData(val page: Int) : ParentDataModifier {
  override fun Density.modifyParentData(parentData: Any?): Any = this@PageData
}

private val Measurable.page: Int
  get() = (parentData as? PageData)?.page ?: error("no PageData for measurable $this")

@Composable
fun Pager(
  state: PagerState,
  modifier: Modifier = Modifier,
  offscreenLimit: Int = 2,
  pageContent: @Composable PagerScope.() -> Unit
) {
  var pageSize by remember { mutableStateOf(0) }
  Layout(
    content = {
      val minPage = (state.currentPage - offscreenLimit).coerceAtLeast(state.minPage)
      val maxPage = (state.currentPage + offscreenLimit).coerceAtMost(state.maxPage)

      for (page in minPage..maxPage) {
        val pageData = PageData(page)
        val scope = PagerScope(state, page)
        key(pageData) {
          Box(contentAlignment = Alignment.Center, modifier = pageData) {
            scope.pageContent()
          }
        }
      }
    },
    modifier = modifier.draggable(
      orientation = Orientation.Horizontal,
      onDragStarted = {
        state.selectionState = PagerState.SelectionState.Undecided
      },
      onDragStopped = { velocity ->
        // Velocity is in pixels per second, but we deal in percentage offsets, so we
        // need to scale the velocity to match
        state.fling(velocity / pageSize)
      }
    ) { dy ->
      with(state) {
        val pos = pageSize * currentPageOffset
        val max = if (currentPage == minPage) 0 else pageSize * offscreenLimit
        val min = if (currentPage == maxPage) 0 else -pageSize * offscreenLimit
        val newPos = (pos + dy).coerceIn(min.toFloat(), max.toFloat())
        currentPageOffset = newPos / pageSize
      }
    }
  ) { measurables, constraints ->
    layout(constraints.maxWidth, constraints.maxHeight) {
      val currentPage = state.currentPage
      val offset = state.currentPageOffset
      val childConstraints = constraints.copy(minWidth = 0, minHeight = 0)

      measurables
        .map {
          it.measure(childConstraints) to it.page
        }
        .forEach { (placeable, page) ->
          // TODO: current this centers each page. We should investigate reading
          //  gravity modifiers on the child, or maybe as a param to Pager.
          val xCenterOffset = (constraints.maxWidth - placeable.width) / 2
          val yCenterOffset = (constraints.maxHeight - placeable.height) / 2

          if (currentPage == page) {
            pageSize = placeable.width
          }

          val xItemOffset = ((page + offset - currentPage) * placeable.width).roundToInt()

          placeable.place(
            x = xCenterOffset + xItemOffset,
            y = yCenterOffset
          )
        }
    }
  }
}

/**
 * Scope for [Pager] content.
 */
class PagerScope(
  private val state: PagerState,
  val page: Int
) {
  /**
   * Returns the current selected page
   */
  val currentPage: Int
    get() = state.currentPage

  /**
   * Returns the current selected page offset
   */
  val currentPageOffset: Float
    get() = state.currentPageOffset

  /**
   * Returns the current selection state
   */
  val selectionState: PagerState.SelectionState
    get() = state.selectionState
}
